
%include "boot.inc"

section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR

; 在 mbr.S 中: jmp LOADER_BASE_ADDR + 0x300
; 所以这里不需要了，否则需要 0xb03 才可以查看到内存
;jmp loader_start ; 占用 2 or 3 个字节

; 构建 GDT 及其内部的描述符
GDT_BASE:   dd 0x0000000
            dd 0x0000000

CODE_DESC:  dd 0x0000FFFF
            dd DESC_CODE_HIGH4

DATA_STACK_DESC:    dd 0x0000FFFF
                    dd DESC_DATA_HIGH4

VIDEO_DESC: dd 0x80000007
            dd DESC_VIDEO_HIGH4

GDT_SIZE equ $ - GDT_BASE
GDT_LIMIT equ GDT_SIZE - 1
times 60 dq 0
SELECTOR_CODE  equ (0x0001 << 3) + TI_GDT + RPL0
SELECTOR_DATA  equ (0x0002 << 3) + TI_GDT + RPL0
SELECTOR_VIDEO equ (0x0003 << 3) + TI_GDT + RPL0

; 当前偏移 loader.bin 文件头为 0x200h, loader.bin 的加载地址为 0x900, 故 total_mem_bytes 内存中的地址是 0xb00.
; 将来的内核中会引用此地址
total_mem_bytes dd 0 ; 用于保存内存容量，字节单位

gdt_ptr dw GDT_LIMIT
        dd GDT_BASE

; 人工对齐：total_mem_bytes(4) + gdt_ptr(6) + ards_buf(244) + ards_nr2(2) = 256 字节
ards_buf times 244 db 0
ards_nr dw 0

loader_start:
    xor ebx, ebx        ; 下一个 ARDS 的地址, 首次需置为 0
    mov edx, 0x534d4150 ; 固定的签名标记
    mov di, ards_buf    ; es:di ARDS 缓冲区，保存读入的内存信息
.e820_mem_get_loop:
    mov eax, 0x0000e820 ; 子功能号
    mov ecx, 20         ; ARDS 结构的字节大小
    int 0x15
    jc .e820_failed_so_try_e801 ; CF=0为未出错，否则调用出错
    add di, cx          ; 指向下一个 ARDS
    inc word [ards_nr]  ; 记录 ARDS 的数量
    cmp ebx, 0          ; 若 ebx=0, 则表示当前是最后一个 ARDS
    jnz .e820_mem_get_loop

; 找出所有 ARDS 中内存容量最大的
    mov cx, [ards_nr]
    mov ebx, ards_buf
    xor edx, edx
.find_max_mem_area:
    mov eax, [ebx]      ; baseAddLow
    add eax, [ebx + 8]  ; lengthLow
    add ebx, 20         ; 指向缓冲区中下一个 ARDS
    cmp edx, eax        ; 冒泡排序，直到找到最大
    jge .next_ards
    mov edx, eax        ; edx 为总内存大小
.next_ards:
    loop .find_max_mem_area
    jmp .mem_get_ok

;----------------------------------
; int 15h
; 功能号：AX=E801H
; 获取内存大小，最大支持 4G
;----------------------------------
.e820_failed_so_try_e801:
    mov ax, 0xe801
    int 0x15
    jc .e801_failed_so_try88

    mov cx, 0x400
    mul cx ; 乘 1024 变成 MB
    shl edx, 16
    and eax, 0x0000FFFF
    or edx, eax
    add edx, 0x100000
    mov esi, edx

    xor eax, eax
    mov ax, bx
    ;mov ecx, 0x10000
    mov ecx, 0x400
    mul ecx
    add esi, eax ; 高 32 位 edx 必然为 0，因此只加 eax 即可
    mov edx, esi
    jmp .mem_get_ok

; --------------------------------------------
; int 15h, 功能号：AH=0x88h
; 获取内存大小，只能获取 64M 之内
; --------------------------------------------
.e801_failed_so_try88:
    mov ah, 0x88
    int 0x15
    jc .error_hlt
    and eax, 0x0000FFFF

    mov cx, 0x400
    mul cx
    shl edx, 16
    or edx, eax
    add edx, 0x100000

.mem_get_ok:
    ;mov ebp, total_mem_bytes
    mov [total_mem_bytes], edx

; -----------------进入保护模式--------------------

    in al, 0x92
    or al, 0000_0010B
    out 0x92, al

    lgdt [gdt_ptr]

    mov eax, cr0
    or eax, 0x00000001
    mov cr0, eax

    jmp dword SELECTOR_CODE:p_mode_start

.error_hlt:
    hlt

[bits 32]
p_mode_start:
    mov ax, SELECTOR_DATA
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov esp, LOADER_STACK_TOP
    mov ax, SELECTOR_VIDEO
    mov gs, ax

    ; 安装分页
    call setup_page

    ; 要将描述符表地址及偏移量写入内存 gdt_ptr, 一会用新地址加载
    sgdt [gdt_ptr] ; 保存 GDT 描述符

    ; 将 GDT 描述符中视频段描述符中的段基地址 +0xc0000000
    mov ebx, [gdt_ptr + 2]
    or dword [ebx + 0x18 + 4], 0xc0000000

    ; 将 GDT 的基址加上 0xc0000000 使其成为内核所在的高地址
    add dword [gdt_ptr + 2], 0xc0000000

    add esp, 0xc0000000 ; 将栈指针同样映射到内核地址

    ; 把页目录地址赋值给 CR3
    mov eax, PAGE_DIR_TABLE_POS
    mov cr3, eax

    ; 打开 CR0 的 PG 位(第 31 位)
    mov eax, cr0
    or eax, 0x80000000
    mov cr0, eax

    ; 在开启分页后，用 GDT 新的地址重新加载
    lgdt [gdt_ptr] ; 重新加载

    mov byte [gs:160], 'V'

    jmp $
    
;-------------   创建页目录及页表   ---------------
setup_page:
    ; 先把页目录占用的空间逐字节清 0
    mov ecx, 4096
    mov esi, 0
.clear_page_dir:
    mov byte [PAGE_DIR_TABLE_POS + esi], 0
    inc esi
    loop .clear_page_dir

; 开始创建页目录项(PDE)
.create_pde:
    mov eax, PAGE_DIR_TABLE_POS
    add eax, 0x1000             ; 此时表示第一个页表的位置及属性(属性指的是PDE的高20位，位置指的是PDE所对应的页表物理地址)
    mov ebx, eax                ; 保留第一个页表的物理地址，为 .create_pte 做准备，ebx 为基址
    
    or eax, PG_US_U | PG_RW_W | PG_P ; 设置 PDE 低 12 位的属性
    ; 将 0x0000 和 0x0c00 都指向第一个页表
    ; 这样使 0xC03FFFFF 以下的地址和 0x003FFFFF 以下的地址都指向相同的页表
    ; 这是为了将地址映射为内核地址做准备
    mov [PAGE_DIR_TABLE_POS + 0x0000], eax
    mov [PAGE_DIR_TABLE_POS + 0x0c00], eax ; 0xc00 表示第 768 个目录项
    sub eax, 0x1000

    mov [PAGE_DIR_TABLE_POS + 4092], eax ; 最后一个页目录项指向页目录表自己

; 下面创建页表项(PTE)
    mov ecx, 256 ; 1M 低端内存 / 每页大小 4k = 256
                 ; 一个页表共 4MB，目前只要低端的 1MB，用于映射内核？
    mov esi, 0
    mov edx, PG_US_U | PG_RW_W | PG_P
.create_pte:
    mov [ebx + esi * 4], edx ; ebx 上面已经赋值过了，指向的是第一个页表的位置
    add edx, 4096            ; 指向下一个页表项
                             ; 看的出来其物理地址(PTE高20位)是连续分配的，即在低端1MB内存中，虚拟地址等于物理地址
    inc esi
    loop .create_pte

; 创建内核其它页表的PDE，也就是页目录表剩下所有的页目录项
    mov eax, PAGE_DIR_TABLE_POS
    add eax, 0x2000 ; 此时 eax 为第二个页表的位置
    or eax, PG_US_U | PG_RW_W | PG_P
    mov ebx, PAGE_DIR_TABLE_POS
    mov ecx, 254 ; 范围为 769~1022 的所有目录项数量
    mov esi, 769
.create_kernel_pde:
    mov [ebx + esi * 4], eax
    inc esi
    add eax, 0x1000 ; 指向下一个页表
    loop .create_kernel_pde
    ret
